﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EFCoreBest.Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace EFCoreBest.Controllers
{
    [ApiController]
    [Route("test")]
    public class TestController : ControllerBase
    {
        private readonly TestDbContext _db;
        private readonly ILogger _logger;

        public TestController(TestDbContext db, ILogger<TestDbContext> logger)
        {
            _db = db;
            _logger = logger;
        }

        #region Add



        #endregion

        #region Update

        [HttpPost("product")]
        public async Task<Product> UpdateProduct(Product p)
        {
            _db.Update(p);
            await _db.SaveChangesAsync(HttpContext.RequestAborted);
            /*
             UPDATE `Products` SET `CreateAt` = @p0, `Name` = @p1, `Price` = @p2, `Stock` = @p3
             WHERE `Id` = @p4 AND `Stock` = @p5;
             SELECT ROW_COUNT();
             */
            return p;
        }

        [HttpPost("product/name")]
        public async Task<Product> UpdateProductName(Product p)
        {
            _db.Entry(p).Property(x => x.Name).IsModified = true;
            await _db.SaveChangesAsync(HttpContext.RequestAborted);
            /*
             * UPDATE `Products` SET `Name` = @p0
             * WHERE `Id` = @p1 AND `Stock` = @p2;
             * SELECT ROW_COUNT();
             */
            return p;
        }

        [HttpPost("product/{id}/price/{price}")]
        public async Task<Product> UpdateProductPrice(int id, decimal price)
        {
            var product = await _db.Products.FindAsync(new object[] { id }, HttpContext.RequestAborted);
            product.Price = price;

            // product 是以Tracking模式从数据库获取，无需调用Update
            // _db.Update(product);

            await _db.SaveChangesAsync(HttpContext.RequestAborted);
            /*
             *  SQL:
             *   UPDATE `Products` SET `Price` = @p0
             *   WHERE `Id` = @p1 AND `Stock` = @p2;
             *   SELECT ROW_COUNT
             */

            return product;
        }

        [HttpPost("product/sell/{id}/stock/{stock}")]
        public async Task<IActionResult> Sell(int id, int stock)
        {
            while (true)
            {
                var product = await _db.Products
                    .FirstOrDefaultAsync(x => x.Id == id, HttpContext.RequestAborted);
                product.Stock -= stock;
                if (product.Stock < 0)
                {
                    return BadRequest();
                }

                try
                {
                    // Attempt to save changes to the database
                    await _db.SaveChangesAsync();
                    return Ok();

                }
                catch (DbUpdateConcurrencyException)
                {
                    _db.Entry(product).State = EntityState.Detached;
                }
            }
        }

        #endregion

        #region Import

        [HttpPost("/blog/import/{count}")]
        public async Task<long> ImportBlog(int count)
        {
            var sw = new Stopwatch();
            sw.Start();
            _db.ChangeTracker.AutoDetectChangesEnabled = false;
            try
            {
                var products = Enumerable.Range(0, count)
                    .Select(_ => new Blog
                    {
                        Name = "P_Import"
                    });
                /*
                 * 前提：
                 * 1. msyql.MaxBatchSize(xxx)
                 * 2. Blog 的主键不是自增
                 */
                await _db.AddRangeAsync(products, HttpContext.RequestAborted);
                await _db.SaveChangesAsync(HttpContext.RequestAborted);

                /*
      Executed DbCommand (1ms) [Parameters=[@p0='08d89cae-d37d-4785-84d7-1f2ae743b619', @p1='P_Import' (Nullable = false) (Size = 100), @p2='08d89cae-d37d-47ad-8881-51f9f61d2638', @p3='P_Import' (Nullable = false) (Size = 100), @p4='08d89cae-d37d-47b8-8610-31cab6570baa', @p5='P_Import' (Nullable = false) (Size = 100), @p6='08d89cae-d37d-47c0-8c3b-480d4bc9ff8e', @p7='P_Import' (Nullable = false) (Size = 100)], CommandType='Text', CommandTimeout='30']
      INSERT INTO `Blogs` (`Id`, `Name`)
      VALUES (@p0, @p1),
      (@p2, @p3),
      (@p4, @p5),
      (@p6, @p7);
                 */
            }
            finally
            {
                _db.ChangeTracker.AutoDetectChangesEnabled = true;
            }
            sw.Stop();
            return sw.ElapsedMilliseconds;
        }

        [HttpPost("/order/import/{count}")]
        public async Task<long> ImportOrder(int count)
        {
            var sw = new Stopwatch();
            sw.Start();
            _db.ChangeTracker.AutoDetectChangesEnabled = false;
            var random = new Random();
            try
            {
                var products = Enumerable.Range(0, count)
                    .Select(_ => new Order
                    {
                        UserId = random.Next(70000, 90000)
                    });
                /*
                 * 前提：
                 * 1. msyql.MaxBatchSize(xxx)
                 */
                await _db.AddRangeAsync(products, HttpContext.RequestAborted);
                await _db.SaveChangesAsync(HttpContext.RequestAborted);
                /*
      INSERT INTO `Orders` (`UserId`)
      VALUES (@p521);
      SELECT `Id`, `CreateAt`
      FROM `Orders`
      WHERE ROW_COUNT() = 1 AND `Id` = LAST_INSERT_ID();

      INSERT INTO `Orders` (`UserId`)
      VALUES (@p522);
      SELECT `Id`, `CreateAt`
      FROM `Orders`
      WHERE ROW_COUNT() = 1 AND `Id` = LAST_INSERT_ID();
                 */
            }
            finally
            {
                _db.ChangeTracker.AutoDetectChangesEnabled = true;
            }
            sw.Stop();
            return sw.ElapsedMilliseconds;
        }

        #endregion

        #region Query

        [HttpGet("product/find/{id}")]
        public async Task<Product> FindProduct(int id)
        {
            var p1 = await _db.Products.FindAsync(new object[] { id }, HttpContext.RequestAborted);

            using var scope = HttpContext.RequestServices.CreateScope();
            var temDb = scope.ServiceProvider.GetService<TestDbContext>();
            var p2 = await temDb.Products.FindAsync(new object[] { id }, HttpContext.RequestAborted);

            var p3 = await _db.Products.FindAsync(new object[] { id }, HttpContext.RequestAborted);

            _logger.LogInformation($"p1 == p2: {p1.Equals(p2)}; p1 == p3: {p1.Equals(p3)}");
            // Output:
            //   p1 == p2: false; p1 == p3: true
            return p3;
        }

        [HttpGet("product/{id}")]
        public async Task<Product> GetProduct(int id)
        {
            var p1 = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, HttpContext.RequestAborted);
            var p2 = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, HttpContext.RequestAborted);
            _logger.LogInformation($"p1 == p2: {p1.Equals(p2)}");
            // Output:
            //   p1 == p2: false
            return p2;
        }

        [HttpGet("no-tracking")]
        public async Task<object> NoTracking()
        {
            _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            try
            {
                var query = _db.Users
                    .Include(x => x.Orders)
                    .Where(x => x.Orders.Count > 0)
                    .Select(x => new
                    {
                        x.Id,
                        x.Name,
                        x.Orders.Count
                    });
                /*
     SELECT `u`.`Id`, `u`.`Name`, (
          SELECT COUNT(*)
          FROM `Orders` AS `o`
          WHERE `u`.`Id` = `o`.`UserId`) AS `Count`
      FROM `Users` AS `u`
      WHERE (
          SELECT COUNT(*)
          FROM `Orders` AS `o0`
          WHERE `u`.`Id` = `o0`.`UserId`) > 0;
                 */
                var result = await query
                        .ToListAsync(HttpContext.RequestAborted);

                return result;

            }
            finally
            {
                _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
            }
        }

        [HttpGet("split")]
        public async Task<object> Split()
        {
            _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            try
            {
                var query = _db.Users
                    .Include(x => x.Orders);
                /*

      SELECT `u`.`Id`, `u`.`CreateAt`, `u`.`Name`
      FROM `Users` AS `u`
      ORDER BY `u`.`Id`

      SELECT `o`.`Id`, `o`.`CreateAt`, `o`.`UserId`, `u`.`Id`
      FROM `Users` AS `u`
      INNER JOIN `Orders` AS `o` ON `u`.`Id` = `o`.`UserId`
      ORDER BY `u`.`Id`
                 */
                var result = await query
                    .AsSplitQuery()
                    .ToListAsync(HttpContext.RequestAborted);

                return result;

            }
            finally
            {
                _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
            }
        }

        [HttpGet("like")]
        public async Task<object> Like()
        {
            return await _db.Users
                .AsNoTracking()
                .Where(x => EF.Functions.Like(x.Name, "%yc%"))
                .FirstOrDefaultAsync(HttpContext.RequestAborted);
            /*
      SELECT `u`.`Id`, `u`.`CreateAt`, `u`.`Name`
      FROM `Users` AS `u`
      WHERE `u`.`Name` LIKE '%yc%'
      LIMIT 1
             */
        }

        private static Func<TestDbContext, int, CancellationToken, Task<User>> _getUserById
            = EF.CompileAsyncQuery<TestDbContext, int, User>((context, id, cancel)
                => context.Users.FirstOrDefault(x => x.Id == id));

        [HttpGet("compile")]
        public async Task<User> Compile()
        {
            return await _getUserById(_db, 1, HttpContext.RequestAborted);
        }

        #endregion
    }
}
